Dynamic visualization of graphs using d3.js


In [1]:
from IPython.html import widgets
from IPython.display import display
from eventful_graph import EventfulGraph
from widget_forcedirectedgraph import ForceDirectedGraphWidget, publish_js
publish_js()


d3.js has excellent force directed graph layout algorithms. But most network/graph data is not in the browser.

NetworkX is a Python library that provides graph data structures, algorithms and visualization.

In this example, we demonstrate how it is possible to hook up NetworkX graph data strutures in Python to d3 based visualizations in a way that allows the dynamic visualization of graphs.

The goal of this demo is to show how Widgets allow state to be sent to the browser in real time.

Here is an outline of the moving parts:

  • EventfulGraph is a subclass of networkx.Graph that can trigger events when the graph data changes
    • Add and remove node and edges
    • Metadata/property/attributes changes (color, radius, label, etc.)
  • ForceDirectedGraphWidget is an IPython widget that watches an EventfulGraph for events and synchronizes the graph data to a JavaScript model in the browser
  • The JavaScript view for the ForceDirectedGraphWidget uses d3's force directed layout algorithms.
  • Python code can modify the graph object and the view will update in real time.

A simple example

Here we setup the Widgets and create an EventfulGraph:


In [2]:
floating_container = widgets.PopupWidget(default_view_name='ModalView')
floating_container.description = "Dynamic D3 rendering of a NetworkX graph"
floating_container.button_text = "Render Window"
floating_container.set_css({
    'width': '420px',
    'height': '350px'}, selector='modal')

G = EventfulGraph()
d3 = ForceDirectedGraphWidget(G)

floating_container.children = [d3]
display(floating_container)

The following code animates some of the graphs's attributes:


In [3]:
import time

G.add_node(1, fill="red", stroke="black", color='black', label='A')
time.sleep(1.0)

G.add_node(2, fill="gold", stroke="black", color='black', r=20, font_size='24pt', label='B')
time.sleep(1.0)

G.add_node(3, fill="green", stroke="black", color='white', label='C')
time.sleep(1.0)

G.add_edges_from([(1,2),(1,3), (2,3)], stroke="#aaa", strokewidth="1px", distance=200, strength=0.5)
time.sleep(1.0)

G.adj[1][2]['distance'] = 20
time.sleep(1.0)

G.adj[1][3]['distance'] = 20
time.sleep(1.0)

G.adj[2][3]['distance'] = 20
time.sleep(1.0)

G.node[1]['r'] = 16
time.sleep(0.3)
G.node[1]['r'] = 8
G.node[2]['r'] = 16
time.sleep(0.3)
G.node[2]['r'] = 20
G.node[3]['r'] = 16
time.sleep(0.3)
G.node[3]['r'] = 8

G.node[1]['fill'] = 'purple'
time.sleep(0.3)
G.node[1]['fill'] = 'red'
G.node[2]['fill'] = 'purple'
time.sleep(0.3)
G.node[2]['fill'] = 'gold'
G.node[3]['fill'] = 'purple'
time.sleep(0.3)
G.node[3]['fill'] = 'green'
time.sleep(1.0)

G.node.clear()
time.sleep(1.0)

floating_container.close()

Visualizing integer factoring

In this example, we visualize the factoring of small integers. We use a simple algorithm:

  • For a given integer find all of its divisors in increasing order: $10 \rightarrow 2, 5$
  • For all of those divisors recursively find their divisors
  • Continue until all of the divisors are prime
  • Nodes in the graph are the divisors
  • Edges are added between an integer and each of its divisors

In [4]:
BACKGROUND = '#FFFFFF'
PARENT_COLOR = '#3E5970'
FACTOR_COLOR = '#424357'
EDGE_COLOR = '#000000'
PRIME_COLOR = '#FF5555'

existing_graphs = []

def handle_graph(graph):
    if len(existing_graphs) > 0:
        for graph_popup in existing_graphs:
            graph_popup.close()
        del existing_graphs[:]
        
    floating_container = widgets.ContainerWidget()
    floating_container.description = "Factors"
    floating_container.button_text = "Factors"
    floating_container.set_css({
        'width': '620px',
        'height': '450px'}, selector='modal')
        
    d3 = ForceDirectedGraphWidget(graph)
    d3.charge = -400.
    floating_container.children = [d3]
    floating_container.set_css('background', BACKGROUND)
    d3.width = 600
    d3.height = 400
    display(floating_container)
    existing_graphs.append(floating_container)
EventfulGraph.on_constructed(handle_graph)

In [5]:
CHARGE = -200
MIN_NODE_RADIUS = 15.0
START_NODE_RADIUS = 65.0
is_int = lambda x: int(x) == x
factor = lambda x: [i + 1 for i in range(x-1) if  i != 0 and is_int(x / (float(i) + 1.0))]
calc_node_size = lambda x, start_x: max(float(x)/start_x * START_NODE_RADIUS, MIN_NODE_RADIUS)
calc_edge_length = lambda x, parent_x, start_x: calc_node_size(x, start_x) + calc_node_size(parent_x, start_x)
    
def add_node(graph, value, **kwargs):
    graph.add_node(len(graph.node), charge=CHARGE, strokewidth=0, value=value, label=value, font_size='18pt', dy='8', **kwargs)
    return len(graph.node) - 1
    
def add_child_node(graph, x, number, start_number, parent):
    index = add_node(graph, x, fill=FACTOR_COLOR, r='%.2fpx' % calc_node_size(x, start_number))
    graph.add_edge(index, parent, distance=calc_edge_length(x, number, start_number), stroke=EDGE_COLOR, strokewidth='3px')
    return index

def plot_primes(number, start_number=None, parent=None, graph=None, delay=0.0):
    start_number = start_number or number
    graph = graph or EventfulGraph(sleep=delay)
    parent = parent or add_node(graph, number, fill=PARENT_COLOR, r='%.2fpx' % START_NODE_RADIUS)
    
    factors = factor(number)
    if len(factors) == 0:
        graph.node[parent]['fill'] = PRIME_COLOR
    for x in factors:
        index = add_child_node(graph, x, number, start_number, parent)
        plot_primes(x, start_number, parent=index, graph=graph)

In [6]:
box = widgets.ContainerWidget()
header = widgets.HTMLWidget(value="<h1>Integer Factorizer</h1><br>")
number = widgets.IntSliderWidget(description="Number:", value=100)
speed = widgets.FloatSliderWidget(description="Delay:", min=0.0, max=0.2, value=0.1, step=0.01)

subbox = widgets.ContainerWidget()
button = widgets.ButtonWidget(description="Calculate")
subbox.children = [button]

box.children = [header, number, speed, subbox]
display(box)

box.add_class('align-center')
box.add_class('center')
box.add_class('well well-small')
box.set_css('width', 'auto')

subbox.remove_class('vbox')
subbox.add_class('hbox')
# subbox.add_class('end')

def handle_caclulate(sender):
    plot_primes(number.value, delay=speed.value)
button.on_click(handle_caclulate)

In [6]:


In [ ]: